# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000, 2001 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307	USA

from math import sin, cos, atan2, hypot, pi, fmod, floor

from Sketch.const import ArcArc, ArcChord, ArcPieSlice, ConstraintMask, \
     AlternateMask

from Sketch import _, Point, Polar, Trafo, SingularMatrix, Rect, UnionRects, \
     CreateMultiUndo, NullUndo, RegisterCommands

import graphics

from Sketch.UI.command import AddCmd
import Sketch.UI.skpixmaps
pixmaps = Sketch.UI.skpixmaps.PixmapTk

import handle
from base import Primitive, RectangularPrimitive, RectangularCreator, Creator,\
     Editor
from bezier import PolyBezier
from blend import Blend
from properties import DefaultGraphicsProperties

from Sketch import _sketch



#
#	Ellipse
#

# helper function for snapping (might be useful elsewhere too):
def snap_to_line(start, end, p):
    if start != end:
        result = [(abs(start - p), start), (abs(end - p), end)]
        v = end - start
        length = abs(v)
        r = (v * (p - start)) / (length ** 2)
        if 0 <= r <= 1.0:
            p2 = start + r * v
            result.append((abs(p2 - p), p2))
        return min(result)
    else:
        return (abs(start - p), start)



class Ellipse(RectangularPrimitive):

    is_Ellipse = 1
    is_curve = 1
    is_clip = 1
    has_edit_mode = 1

    commands = RectangularPrimitive.commands[:]

    def __init__(self, trafo = None, start_angle = 0.0, end_angle = 0.0,
                 arc_type = ArcPieSlice, properties = None, duplicate = None):
        if duplicate is not None:
            self.start_angle = duplicate.start_angle
            self.end_angle = duplicate.end_angle
            self.arc_type = duplicate.arc_type
        else:
            self.start_angle = start_angle
            self.end_angle = end_angle
            self.arc_type = arc_type
        RectangularPrimitive.__init__(self, trafo, properties = properties,
                                      duplicate = duplicate)
        self.normalize()

    def DrawShape(self, device, rect = None, clip = 0):
        Primitive.DrawShape(self, device)
        device.SimpleEllipse(self.trafo, self.start_angle, self.end_angle,
                             self.arc_type, rect, clip)

    def SetAngles(self, start_angle, end_angle):
        undo = (self.SetAngles, self.start_angle, self.end_angle)
        self.start_angle = start_angle
        self.end_angle = end_angle
        self.normalize()
        self._changed()
        return undo

    def Angles(self):
        return self.start_angle, self.end_angle

    def SetArcType(self, arc_type):
        if arc_type == self.arc_type:
            return NullUndo
        undo = (self.SetArcType, self.arc_type)
        self.arc_type = arc_type
        self._changed()
        return undo

    def ArcType(self):
        return self.arc_type

    AddCmd(commands, 'EllipseArc', _("Arc"), SetArcType, args = ArcArc)
    AddCmd(commands, 'EllipseChord', _("Chord"), SetArcType, args = ArcChord)
    AddCmd(commands, 'EllipsePieSlice', _("Pie Slice"), SetArcType,
           args = ArcPieSlice)

    def normalize(self):
        pi2 = 2 * pi
        self.start_angle = fmod(self.start_angle, pi2)
        if self.start_angle < 0:
            self.start_angle = self.start_angle + pi2
        self.end_angle = fmod(self.end_angle, pi2)
        if self.end_angle < 0:
            self.end_angle = self.end_angle + pi2

    def Paths(self):
        path = _sketch.approx_arc(self.start_angle, self.end_angle,
                                  self.arc_type)
        path.Transform(self.trafo)
        return (path,)

    def AsBezier(self):
        return PolyBezier(paths = self.Paths(),
                          properties = self.properties.Duplicate())

    def Hit(self, p, rect, device, clip = 0):
        return device.SimpleEllipseHit(p, self.trafo, self.start_angle,
                                       self.end_angle, self.arc_type,
                                       self.properties, self.Filled() or clip,
                                       ignore_outline_mode = clip)

    def Blend(self, other, p, q):
        blended = RectangularPrimitive.Blend(self, other, p, q)
        if self.start_angle != self.end_angle \
           or other.start_angle != other.end_angle:
            blended.start_angle = p * self.start_angle + q * other.start_angle
            blended.end_angle = p * self.end_angle + q * other.end_angle
            if self.start_angle == self.end_angle:
                blended.arc_type = other.arc_type
            elif other.start_angle == other.end_angle:
                blended.arc_type = self.arc_type
            else:
                if self.arc_type == other.arc_type:
                    blended.arc_type = self.arc_type
                # The rest of the arc type blends is quite arbitrary
                # XXX: are these rules acceptable? Maybe we should blend
                # the ellipses as bezier curves if the arc types differ
                elif self.arc_type == ArcArc or other.arc_type == ArcArc:
                    blended.arc_type = ArcArc
                elif self.arc_type == ArcChord or other.arc_type == ArcChord:
                    blended.arc_type = ArcChord
                else:
                    blended.arc_type = ArcPieSlice
        return blended

    def GetSnapPoints(self):
        t = self.trafo
        start_angle = self.start_angle; end_angle = self.end_angle
        if self.start_angle == self.end_angle:
            a = Point(t.m11, t.m21)
            b = Point(t.m12, t.m22)
            c = t.offset()
            return [c, c + a, c - a, c + b, c - b]
        else:
            points = [t(Polar(start_angle)), t(Polar(end_angle)), t.offset()]
            if end_angle < start_angle:
                end_angle = end_angle + 2 * pi
            pi2 = pi / 2
            angle = pi2 * (floor(start_angle / pi2) + 1)
            while angle < end_angle:
                points.append(t(Polar(1, angle)))
                angle = angle + pi2
            return points

    def Snap(self, p):
        try:
            r, phi = self.trafo.inverse()(p).polar()
            start_angle = self.start_angle; end_angle = self.end_angle
            p2 = self.trafo(Polar(1, phi))
            if start_angle == end_angle:
                result = (abs(p - p2), p2)
            else:
                result = []
                if phi < 0:
                    phi = phi + 2 * pi
                if start_angle < end_angle:
                    between = start_angle <= phi <= end_angle
                else:
                    between = start_angle <= phi or phi <= end_angle
                if between:
                    result.append((abs(p - p2), p2))
                start = self.trafo(Polar(self.start_angle))
                end = self.trafo(Polar(self.end_angle))
                if self.arc_type == ArcArc:
                    result.append((abs(start - p), start))
                    result.append((abs(end - p), end))
                elif self.arc_type == ArcChord:
                    result.append((snap_to_line(start, end, p)))
                elif self.arc_type == ArcPieSlice:
                    center = self.trafo.offset()
                    result.append(snap_to_line(start, center, p))
                    result.append(snap_to_line(end, center, p))
                result = min(result)
            return result
        except SingularMatrix:
            # XXX this case could be handled better.
            return (1e200, p)


    def update_rects(self):
        trafo = self.trafo
        start = trafo.offset()
        # On some systems, atan2 can raise a ValueError if both
        # parameters are 0. In that case, the actual value the of angle
        # is not important since in the computation of p below, the
        # coordinate depending on the angle will always be 0 because
        # both trafo coefficients are 0. So set the angle to 0 in case
        # of an exception.
        try:
            phi1 = atan2(trafo.m12, trafo.m11)
        except ValueError:
            phi1 = 0
        try:
            phi2 = atan2(trafo.m22, trafo.m21)
        except ValueError:
            phi2 = 0
        p = Point(trafo.m11 * cos(phi1) + trafo.m12 * sin(phi1),
                  trafo.m21 * cos(phi2) + trafo.m22 * sin(phi2))
        self.coord_rect = r = Rect(start + p, start - p)
        if self.properties.HasLine():
            width = self.properties.line_width
            r = r.grown(width / 2 + 1)
            # add the bounding boxes of arrows
            if self.arc_type == ArcArc:
                pi2 = pi / 2
                arrow1 = self.properties.line_arrow1
                if arrow1 is not None:
                    pos = trafo(Polar(1, self.start_angle))
                    dir = trafo.DTransform(Polar(1, self.start_angle - pi2))
                    r = UnionRects(r, arrow1.BoundingRect(pos, dir, width))
                arrow2 = self.properties.line_arrow2
                if arrow2 is not None:
                    pos = trafo(Polar(1, self.end_angle))
                    dir = trafo.DTransform(Polar(1, self.end_angle + pi2))
                    r = UnionRects(r, arrow2.BoundingRect(pos, dir, width))
        self.bounding_rect = r

    def Info(self):
        trafo = self.trafo
        w = hypot(trafo.m11, trafo.m21)
        h = hypot(trafo.m12, trafo.m22)
        dict = {'center': trafo.offset(), 'radius': w, 'axes': (w, h)}
        if w == h:
            text = _("Circle radius %(radius)[length], "
                     "center %(center)[position]")
        else:
            text = _("Ellipse axes %(axes)[size], center %(center)[position]")
        return text, dict

    def SaveToFile(self, file):
        Primitive.SaveToFile(self, file)
        file.Ellipse(self.trafo, self.start_angle, self.end_angle,
                     self.arc_type)

    def Editor(self):
        return EllipseEditor(self)

    context_commands = ('EllipseArc', 'EllipseChord', 'EllipsePieSlice')

RegisterCommands(Ellipse)


class EllipseCreator(RectangularCreator):

    creation_text = _("Create Ellipse")

    def compute_trafo(self, state):
        start = self.drag_start
        end = self.drag_cur
        if state & AlternateMask:
            # start is the center of the ellipse
            if state & ConstraintMask:
                # end is a point of the periphery of a *circle* centered
                # at start
                radius = abs(start - end)
                self.trafo = Trafo(radius, 0, 0, radius, start.x, start.y)
            else:
                # end is a corner of the bounding box
                d = end - start
                self.trafo = Trafo(d.x, 0, 0, d.y, start.x, start.y)
        else:
            # the ellipse is inscribed into the rectangle with start and
            # end as opposite corners. 
            end = self.apply_constraint(self.drag_cur, state)
            d = (end - start) / 2
            self.trafo = Trafo(d.x, 0, 0, d.y, start.x + d.x, start.y + d.y)

    def MouseMove(self, p, state):
        # Bypass RectangularCreator
        Creator.MouseMove(self, p, state)
        self.compute_trafo(state)

    def ButtonUp(self, p, button, state):
        Creator.DragStop(self, p)
        self.compute_trafo(state)

    def DrawDragged(self, device, partially):
        device.DrawEllipse(self.trafo(-1, -1), self.trafo(1, 1))

    def CurrentInfoText(self):
        t = self.trafo
        data = {}
        if abs(round(t.m11, 2)) == abs(round(t.m22, 2)):
            text = _("Circle %(radius)[length], center %(center)[position]")
            data['radius'] = t.m11
        else:
            text = _("Ellipse %(size)[size], center %(center)[position]")
            data['size'] = (abs(t.m11), abs(t.m22))
        data['center'] = t.offset()
        return text, data

    def CreatedObject(self):
        return Ellipse(self.trafo,
                       properties = DefaultGraphicsProperties())


class EllipseEditor(Editor):

    EditedClass = Ellipse

    selection = 0

    def ButtonDown(self, p, button, state):
        if self.selection == 1:
            start = self.trafo(cos(self.start_angle), sin(self.start_angle))
        else:
            start = self.trafo(cos(self.end_angle), sin(self.end_angle))
        Editor.DragStart(self, start)
        return p - start

    def apply_constraint(self, p, state):
        if state & ConstraintMask:
            try:
                inverse = self.trafo.inverse()
                p2 = inverse(p)
                r, phi = p2.polar()
                pi12 = pi / 12
                angle = pi12 * floor(phi / pi12 + 0.5)
                pi2 = 2 * pi
                d1 = fmod(abs(phi - angle), pi2)
                if self.selection == 1:
                    selected_angle = self.end_angle
                else:
                    selected_angle = self.start_angle
                d2 = fmod(abs(phi - selected_angle), pi2)
                if d2 < d1:
                    phi = selected_angle
                else:
                    phi = angle
                p = self.trafo(Polar(r, phi))
            except SingularMatrix:
                pass
        return p

    def MouseMove(self, p, state):
        p = self.apply_constraint(p, state)
        Editor.MouseMove(self, p, state)

    def ButtonUp(self, p, button, state):
        p = self.apply_constraint(p, state)
        Editor.DragStop(self, p)
        start_angle, end_angle, arc_type = self.angles()
        return CreateMultiUndo(self.object.SetAngles(start_angle, end_angle),
                               self.object.SetArcType(arc_type))

    def angles(self):
        start_angle = self.start_angle; end_angle = self.end_angle
        if self.arc_type == ArcChord:
            arc_type = ArcChord
        else:
            arc_type = ArcPieSlice

        try:
            inverse = self.trafo.inverse()
            p = inverse(self.drag_cur)
            if self.selection == 1:
                start_angle = atan2(p.y, p.x)
            elif self.selection == 2:
                end_angle = atan2(p.y, p.x)
            if abs(p) > 1:
                arc_type = ArcArc
        except SingularMatrix:
            pass
        if fmod(abs(start_angle - end_angle), 2 * pi) < 0.0001:
            if self.selection == 1:
                start_angle = end_angle
            else:
                end_angle = start_angle
        return (start_angle, end_angle, arc_type)

    def DrawDragged(self, device, partially):
        start_angle, end_angle, arc_type = self.angles()
        device.SimpleEllipse(self.trafo, start_angle, end_angle, arc_type)

    def GetHandles(self):
        trafo = self.trafo
        start_angle = self.start_angle; end_angle = self.end_angle
        p1 = trafo(cos(self.start_angle), sin(self.start_angle))
        if start_angle == end_angle:
            return [handle.MakeNodeHandle(p1)]
        p2 = trafo(cos(self.end_angle), sin(self.end_angle))
        return [handle.MakeNodeHandle(p1), handle.MakeNodeHandle(p2)]

    def SelectHandle(self, handle, mode):
        self.selection = handle.index + 1

    def SelectPoint(self, p, rect, device, mode):
        return 0
